﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.IO;
using System.Text;

namespace DivaScript
{
    /// <summary>
    /// PV script format.
    /// </summary>
    public enum Format : byte
    {
        /// <summary>
        /// Text based PV script
        /// </summary>
        DivaScript,
        /// <summary>
        /// Project DIVA F
        /// </summary>
        F,
        /// <summary>
        /// Project DIVA F 2nd
        /// </summary>
        F2,
        /// <summary>
        /// Project DIVA Dreamy Theater
        /// </summary>
        DT,
        /// <summary>
        /// Project DIVA Future Tone
        /// </summary>
        FT,
        /// <summary>
        /// Project DIVA X
        /// </summary>
        X,
        /// <summary>
        /// Just for Stewie :HeheHehe:
        /// </summary>
        MGF,
        /// <summary>
        /// Project Mirai
        /// </summary>
        Mirai,
        /// <summary>
        /// Project DIVA F 2nd Edit
        /// </summary>
        Edit
    }

    /// <summary>
    /// Project DIVA PV Script.
    /// </summary>
    public class PvScript
    {
        private enum BranchMode
        {
            NULL = -1,
            DEFAULT = 0,
            FAIL = 1,
            SUCCESS = 2,
        }

        private class PvCommandBlock
        {
            public int Time { get; set; }

            public List<PvCommand> Commands { get; set; }

            public BranchMode BranchMode { get; set; }

            public PvCommandBlock(int time, BranchMode branchMode, params PvCommand[] commands)
            {
                Time = time;
                BranchMode = branchMode;
                Commands = new List<PvCommand>(commands);
            }
        }

        public delegate List<PvCommand> ParseFileDel(string path, bool isBigEndian);

        public delegate void WriteFileDel(string outputPath, List<PvCommand> commands, bool isBigEndian);

        public static string CommandsToString(List<PvCommand> commands)
        {
            var sb = new StringBuilder();
            foreach (var command in commands)
            {
                sb.AppendLine(command.ToString());
            }
            return sb.ToString();
        }

        /// <summary>
        /// Writes a DivaScript in a text based form to the outputPath.
        /// </summary>
        public static void WriteTextScript(string outputPath, List<PvCommand> commands)
        {
            var file = File.CreateText(outputPath);

            file.Write(CommandsToString(commands));

            file.Dispose();
        }

        /// <summary>
        /// Parses a text based DivaScript.
        /// </summary>
        public static List<PvCommand> ParseTextFiles(string[] filePaths)
        {
            var commands = new List<PvCommand>();
            foreach (var path in filePaths)
            {
                var fileCommands = ParseCommandStrings(File.ReadAllLines(path));
                foreach (var command in fileCommands)
                {
                    commands.Add(command);
                }
            }
            return (filePaths.Length > 1) ? OrderListByTime(commands) : commands;
        }

        /// <summary>
        /// Returns and matches PV commands from a string array.
        /// </summary>
        public static List<PvCommand> ParseCommandStrings(string[] commandStrings)
        {
            Regex commandCallRX = new Regex(@"[a-z_A-Z]+\([^\)]*\)(\.[^\)]*\))?");
            Regex commandOpcodeRX = new Regex(@"[\w\s$]*\(");
            Regex parameterRX = new Regex(@"-?\d+,? ?");

            var commands = new List<PvCommand>();

            int currTime = 0;
            foreach (var line in commandStrings)
            {
                foreach (var command in commandCallRX.Matches(line))
                {
                    var opcode = commandOpcodeRX.Match(command.ToString()).ToString().Replace("(", "");
                    var param = new List<int>();

                    foreach (var p in parameterRX.Matches(command.ToString()))
                        param.Add(Convert.ToInt32(p.ToString().Replace(",", "")));

                    if (opcode.ToUpper() == "TIME")
                        currTime = param[0];

                    commands.Add(new PvCommand(currTime, opcode, param));
                }
            }

            return commands;
        }

        /// <summary>
        /// Writes a DivaScript in a binary format to the outputPath in the specified format.
        /// </summary>
        public static void WriteBinaryScript(string outputPath, List<PvCommand> commands, Format format, bool isBigEndian = false)
        {
            WriteFileDel writeFile = F2.Script.WriteToFile;

            switch (format)
            {
                case Format.F:
                    writeFile = F.Script.WriteToFile;
                    break;
                case Format.F2:
                    writeFile = F2.Script.WriteToFile;
                    break;
                case Format.DT:
                    //writeFile = DT.Script.WriteToFile;
                    throw new NotImplementedException();
                    break;
                case Format.FT:
                    writeFile = FT.Script.WriteToFile;
                    break;
                case Format.X:
                    writeFile = X.Script.WriteToFile;
                    break;
                case Format.MGF:
                    writeFile = MGF.Script.WriteToFile;
                    break;
                case Format.Mirai:
                    writeFile = Mirai.Script.WriteToFile;
                    break;
                default:
                    break;
            }
            writeFile(outputPath, commands, isBigEndian);
        }

        /// <summary>
        /// Parses a binary based DivaScript in the specified format.
        /// </summary>
        public static List<PvCommand> ParseBinaryScripts(string[] filePaths, Format format, bool isBigEndian = false)
        {
            ParseFileDel parseFile = F2.Script.ParseFile;

            switch (format)
            {
                case Format.F:
                    parseFile = F.Script.ParseFile;
                    break;
                case Format.F2:
                    parseFile = F2.Script.ParseFile;
                    break;
                case Format.DT:
                    //parseFile = DT.Script.ParseFile;
                    throw new NotImplementedException();
                    break;
                case Format.FT:
                    parseFile = FT.Script.ParseFile;
                    break;
                case Format.X:
                    parseFile = X.Script.ParseFile;
                    break;
                case Format.MGF:
                    parseFile = MGF.Script.ParseFile;
                    break;
                case Format.Mirai:
                    parseFile = Mirai.Script.ParseFile;
                    break;
                default:
                    break;
            }

            if (filePaths.Length > 1)
            {
                filePaths = filePaths.OrderByDescending(p => Path.GetFileName(p)).ToArray();
            }

            var unsortedCommandsLists = new List<PvCommand>[filePaths.Length];

            for (int i = 0; i < filePaths.Length; i++)
            {
                unsortedCommandsLists[i] = parseFile(filePaths[i], isBigEndian);
            }

            return (filePaths.Length == 1) ? unsortedCommandsLists[0] : MergeCommands(unsortedCommandsLists, DetermineBarchModes(filePaths));
        }

        private static BranchMode[] DetermineBarchModes(string[] filePaths)
        {
            int fileCount = filePaths.Length;

            string[] fileNames = new string[fileCount];
            bool[] successStates = new bool[fileCount];

            for (int i = 0; i < fileCount; i++)
            {
                fileNames[i] = Path.GetFileNameWithoutExtension(filePaths[i]);
                successStates[i] = containsSuccess(fileNames[i]);
            }

            BranchMode[] modes = new BranchMode[successStates.Length];

            if (successStates.Any(c => c))
            {
                for (int i = 0; i < fileCount; i++)
                    modes[i] = successStates[i] ? BranchMode.SUCCESS : ContainsDifficulty(fileNames[i]) ? BranchMode.DEFAULT : BranchMode.FAIL;
            }

            return modes;

            bool containsSuccess(string fileName) => fileName.Contains("success");
            bool ContainsDifficulty(string fileName) => fileName.Contains("easy") || fileName.Contains("normal") || fileName.Contains("hard") || fileName.Contains("extreme");
        }

        private static List<PvCommand> MergeCommands(List<PvCommand>[] unsortedCommandsLists, BranchMode[] branchModes)
        {
            // order of execution:
            // system -> scene -> mouth -> difficulty

            bool nonDefaultBranch = branchModes.Any(m => m != BranchMode.DEFAULT);

            var commandBlocksArray = new List<PvCommandBlock>[unsortedCommandsLists.Length];

            for (int i = 0; i < unsortedCommandsLists.Length; i++)
            {
                var commandList = unsortedCommandsLists[i];

                var commandBlocks = new List<PvCommandBlock>(commandList.Count);
                commandBlocksArray[i] = commandBlocks;

                BranchMode branchMode = branchModes[i];
                int lastTime = 0;

                for (int c = 0; c < commandList.Count; c++)
                {
                    PvCommand command = commandList[c];
                    int currentTime = command.TimeStamp;

                    if (nonDefaultBranch && command.Opcode == "PV_BRANCH_MODE" && command.Parameters.Count >= 1)
                        branchMode = (BranchMode)command.Parameters[0];

                    if (c == 0 || lastTime != currentTime)
                    {
                        commandBlocks.Add(new PvCommandBlock(currentTime, branchMode, command));
                    }
                    else
                    {
                        commandBlocks[commandBlocks.Count - 1].Commands.Add(command);
                    }

                    lastTime = currentTime;
                }
            }

            int totalCommandCount = unsortedCommandsLists.Sum(c => c.Count);
            List<PvCommand> mergedCommands = new List<PvCommand>(totalCommandCount);

            BranchMode lastBranchMode = BranchMode.NULL;
            bool successBranchStarted = false;

            foreach (var commandBlock in commandBlocksArray.SelectMany(b => b).OrderBy(b => b.Time))
            {
                for (int i = 0; i < commandBlock.Commands.Count; i++)
                {
                    var command = commandBlock.Commands[i];

                    if (nonDefaultBranch && i == 1)
                    {
                        if (commandBlock.BranchMode == BranchMode.SUCCESS)
                            successBranchStarted = true;

                        if (!successBranchStarted && commandBlock.BranchMode == BranchMode.FAIL)
                            commandBlock.BranchMode = BranchMode.DEFAULT;

                        if (lastBranchMode != commandBlock.BranchMode)
                            mergedCommands.Add(new PvCommand(commandBlock.Time, "PV_BRANCH_MODE", (int)commandBlock.BranchMode));

                        lastBranchMode = commandBlock.BranchMode;
                    }

                    if (command.Opcode != "END" && (!nonDefaultBranch || command.Opcode != "PV_BRANCH_MODE"))
                        mergedCommands.Add(command);
                }
            }

            mergedCommands.Add(new PvCommand((mergedCommands.LastOrDefault()?.TimeStamp).GetValueOrDefault(), "END"));

            return mergedCommands;
        }

        /// <summary>
        /// Returns a sorted PvCommand list sorted by their timestamps and opcodes.
        /// </summary>
        public static List<PvCommand> OrderListByTime(List<PvCommand> commands)
        {
            return commands.OrderBy(t => t.TimeStamp).ThenBy(o => o.GenericCommand).ToList();
        }

        /// <summary>
        /// Adds a new TIME command foreach command in the list.
        /// </summary>
        public static List<PvCommand> AddTimeCommands(List<PvCommand> commands)
        {
            commands = OrderListByTime(commands);
            var timeCommands = new List<PvCommand>();
            int time = int.MinValue;
            foreach (var command in commands)
            {
                if (command.Opcode.ToUpper() != "TIME")
                {
                    if (command.TimeStamp != time)
                    {
                        timeCommands.Add(new PvCommand(command.TimeStamp, "TIME", new List<int> { command.TimeStamp }));
                    }
                    time = command.TimeStamp;
                }
            }
            commands.AddRange(timeCommands);
            return commands;
        }
    }
}
